using System;
using System.Collections.Generic;
using System.Reflection;

namespace Fadd.Validation
{
    /// <summary>
    /// Contains all validations for a certain type of object.
    /// </summary>
    public class ObjectValidator
    {
        private static readonly Dictionary<Type, object> _validators = new Dictionary<Type, object>(); // a list with all object validators.

        /// <summary>
        /// true if we should save a list with all generated validators, or generate new ones each time.
        /// </summary>
        public static bool CacheValidators = true;

        private readonly Dictionary<string, List<ValidateAttribute>> _validations = new Dictionary<string, List<ValidateAttribute>>();
        private Type _type;
        private readonly List<string> _propertyNames = new List<string>();

        private ObjectValidator()
        {}

        /// <summary>
        /// Type that this validator is for.
        /// </summary>
        public Type Type
        {
            get { return _type; }
        }

        /// <summary>
        /// Create a new validation object.
        /// </summary>
        /// <param name="type">Type of object to validate</param>
        /// <returns></returns>
        public static ObjectValidator Create(Type type)
        {
            if (CacheValidators)
            {
                object cachedValidator;
                lock (_validators)
                    if (_validators.TryGetValue(type, out cachedValidator))
                        return (ObjectValidator)cachedValidator;
            }

            ObjectValidator validator = new ObjectValidator();
            validator._type = type;

            foreach (PropertyInfo info in validator.Type.GetProperties())
            {
                foreach (object o in info.GetCustomAttributes(true))
                {
                    ValidateAttribute attribute = o as ValidateAttribute;
                    if (attribute == null)
                        continue;

                    List<ValidateAttribute> attributes;
                    if (!validator._validations.TryGetValue(info.Name, out attributes))
                    {
                        attributes = new List<ValidateAttribute>();
                        validator._validations.Add(info.Name, attributes);
                    }

                    if (!validator._propertyNames.Contains(info.Name))
                        validator._propertyNames.Add(info.Name);
                    attributes.Add(attribute);
                }
            }

            if (validator._validations.Count != 0)
            {
                if (CacheValidators)
                {
                    lock (_validators)
                        if (!_validators.ContainsKey(type))
                            _validators.Add(type, validator);
                }
                return validator;
            }

            return null;
        }

        /// <summary>
        /// Validate a specific property.
        /// </summary>
        /// <param name="instance">Instance to validate</param>
        /// <param name="propertyName">Name of property</param>
        /// <returns>A list with all validations that failed. Or an empty list of no validations failed.</returns>
        public IList<ValidationError> Validate(object instance, string propertyName)
        {
            Check.Require(instance, "instance");
            Check.NotEmpty(propertyName, "propertyName");

            List<ValidationError> errors = new List<ValidationError>();
            Validate(instance, propertyName, errors);
            return errors;
        }

        /// <summary>
        /// Validate a specific property.
        /// </summary>
        /// <param name="context">An application specific context that's passed to all validation attributes. Useful if you've implemented your own validations.</param>
        /// <param name="instance">Instance to validate</param>
        /// <param name="propertyName">Name of property</param>
        /// <returns>
        /// A list with all validations that failed. Or an empty list of no validations failed.
        /// </returns>
        public IList<ValidationError> Validate(object context, object instance, string propertyName)
        {
            Check.Require(instance, "instance");
            Check.NotEmpty(propertyName, "propertyName");

            List<ValidationError> errors = new List<ValidationError>();
            Validate(context, instance, propertyName, errors);
            return errors;
        }

        /// <summary>
        /// Validate a specific property.
        /// </summary>
        /// <param name="instance">Instance to validate</param>
        /// <param name="propertyName">Name of property</param>
        /// <param name="errors">List to fill with validation errors, must not be null.</param>
        public void Validate(object instance, string propertyName, IList<ValidationError> errors)
        {
            Check.Require(instance, "instance");
            Check.NotEmpty(propertyName, "propertyName");
            Check.Require(errors, "errors");

            object value = Property.Get(instance, propertyName);
            foreach (ValidateAttribute attribute in _validations[propertyName])
            {
                if (!attribute.Validate(null, value))
                    errors.Add(new ValidationError(propertyName, attribute));
            }
        }

        /// <summary>
        /// Validate a specific property.
        /// </summary>
        /// <param name="context">An application specific context that's passed to all validation attributes. Useful if you've implemented your own validations.</param>
        /// <param name="instance">Instance to validate</param>
        /// <param name="propertyName">Name of property</param>
        /// <param name="errors">List to fill with validation errors, must not be null.</param>
        public void Validate(object context, object instance, string propertyName, IList<ValidationError> errors)
        {
            Check.Require(context, "context");
            Check.Require(instance, "instance");
            Check.NotEmpty(propertyName, "propertyName");
            Check.Require(errors, "errors");

            object value = Property.Get(instance, propertyName);
            foreach (ValidateAttribute attribute in _validations[propertyName])
            {
                if (!attribute.Validate(context, value))
                    errors.Add(new ValidationError(propertyName, attribute));
            }
        }

        /// <summary>
        /// Validate all properties in the object.
        /// </summary>
        /// <param name="instance">Instance to validate</param>
        /// <returns>A list with validation errors, or an empty list of all validations succeeded.</returns>
        public IList<ValidationError> Validate(object instance)
        {
            return Validate(this, instance);
        }

        /// <summary>
        /// Validate all properties in the object.
        /// </summary>
        /// <param name="context">An application specific context that's passed to all validation attributes. Useful if you've implemented your own validations.</param>
        /// <param name="model">Instance to validate</param>
        /// <returns>A list with validation errors, or an empty list of all validations succeeded.</returns>
        public IList<ValidationError> Validate(object context, object model)
        {
            Check.Require(model, "model");
            Check.Type(_type, model, "model");

            List<ValidationError> errors = new List<ValidationError>();
            foreach (string propertyName in _propertyNames)
                Validate(model, propertyName, errors);

            return errors;
        }
    }
}
